trace = True


def rangetest(**argchecks):
    return argtest(argchecks, lambda arg, vals: arg < vals[0] or arg > vals[1])


def typetest(**argchecks):
    return argtest(argchecks, lambda arg, type: not isinstance(arg, type))


def valuetest(**argchecks):
    return argtest(argchecks, lambda arg, tester: not tester(arg))


def argtest(argchecks, failIf):
    def onDecorator(func):
        print('ondecorator:', argchecks)
        if not __debug__:
            return func
        else:
            code = func.__code__
            expected = list(code.co_varnames[:code.co_argcount])
            print('inelse:', expected)

            def onError(argname, criteria):
                errfmt = '%s argument "%s" not %s'
                raise TypeError(errfmt % (func.__name__, argname, criteria))

            def onCall(*pargs, **kargs):                
                positionals = expected[:len(pargs)]
                print('oncall: ', 'pargs:', pargs, 'positionals: ', positionals, 'kargs: ', kargs, 'argchecks: ', argchecks)
                for (argname, criteria) in argchecks.items():
                    if argname in kargs:
                        if failIf(kargs[argname], criteria):
                            onError(argname, criteria)
                    elif argname in positionals:
                        position = positionals.index(argname)
                        if failIf(pargs[position], criteria):
                            onError(pargs[position], criteria)
                    else:
                        if trace:
                            print('Argument "{0}" defaulted'.format(argname))
                return func(*pargs, **kargs)
            return onCall
    return onDecorator


if __name__ == '__main__':
    import sys

    def fails(test):
        try:
            result = test()
        except:
            print('[%s]' % sys.exc_info()[1])
        else:
            print('?%s?' % result)
    print('--------------------------------------------------------------------')

    # @rangetest(m=(1, 12), d=(1, 31), y=(1900, 2013))
    # def date(m, d, y):
    #     print('date = %s/%s/%s' % (m, d, y))
    # date(1, 2, 1960)
    # fails(lambda: date(1, 2, 3))

    # @typetest(a=int, c=float)
    # def sum(a, b, c, d):
    #     print(a + b + c + d)

    # sum(1, 2, 3.0, 4)
    # sum(1, d=4, b=2, c=3.0)
    # fails(lambda: sum('spam', 2, 99, 4))
    # fails(lambda: sum(1, d=4, b=2, c=99))
    # print('--------------------------------------------------------------------')

    # @valuetest(word1=str.islower, word2=(lambda x: x[0].isupper()))
    # def msg(word1='mighty', word2='Larch', label='The'):
    #     print('%s %s %s' % (label, word1, word2))
    # msg()  # word1 and word2 defaulted
    # msg('majestic', 'Moose')
    # fails(lambda: msg('Giant', 'Redwood'))
    # fails(lambda: msg('great', word2='elm'))
    # print('--------------------------------------------------------------------')

    # @valuetest(A=lambda x: isinstance(x, int), B=lambda x: x > 0 and x < 10)
    # def manual(A, B):
    #     print(A + B)
    # manual(100, 2)
    # fails(lambda: manual(1.99, 2))
    # fails(lambda: manual(100, 20))
    # print('--------------------------------------------------------------------')

    @rangetest(X=(1, 10))
    @typetest(Z=str)  # Only innermost validates positional args
    def nester(X, Y, Z):
        return('%s-%s-%s' % (X, Y, Z))
    print(nester(11, 2, 'spam'))  # Original function runs properly
    fails(lambda: nester(1, 2, 3))  # Nested typetest is run: positional
    fails(lambda: nester(1, 2, Z=3))  # Nested typetest is run: keyword
    fails(lambda: nester(0, 2, 'spam'))  # <==Outer rangetest not run: posit.
    # Outer rangetest is run: keyword
    fails(lambda: nester(X=0, Y=2, Z='spam'))
